package codeanticode.clphysics;

import static com.jogamp.opencl.CLMemory.Mem.READ_ONLY;
import static com.jogamp.opencl.CLMemory.Mem.READ_WRITE;
import static com.jogamp.opencl.CLMemory.Mem.WRITE_ONLY;
import static com.jogamp.opencl.CLMemory.Mem.USE_BUFFER;
import static java.lang.Math.min;
import static java.lang.System.nanoTime;
import static java.lang.System.out;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Random;

import processing.core.PApplet;

import com.jogamp.opencl.CLBuffer;
import com.jogamp.opencl.CLKernel;
import com.jogamp.opencl.CLMemory.Mem;
import com.jogamp.opencl.CLProgram;

public class ModifiedEulerIntegrator implements Integrator
{
  ParticleSystem s;
	
	boolean init = false;
	int elementCount;
	int globalWorkSize;
	int localWorkSize;
	CLBuffer<FloatBuffer> clForces;
	CLBuffer<FloatBuffer> clMasses;
	CLBuffer<IntBuffer> clStates;
  CLBuffer<FloatBuffer> clPositions[];
  CLBuffer<FloatBuffer> clVelocities[];	
  CLProgram program;	
  CLKernel kernel;
  int currRead;
  int currWrite;
  
  CLBuffer<FloatBuffer> clBufferA[];
  CLBuffer<FloatBuffer> clBufferB;
	
	public ModifiedEulerIntegrator( ParticleSystem s )
	{
		this.s = s;
	}
	
	public void step( float t )
	{
    s.clearForces();
    s.applyForces();
    
    //PApplet.println("time: " + t);
    float halftt = 0.5f*t*t;    
    
    int n = s.numberOfParticles() * 3;
	  if (!init || elementCount !=  n) {	    	    
      elementCount = n;            
      // Length of arrays to process
      localWorkSize = min(s.device.getMaxWorkGroupSize(), 256);      // Local work size dimensions      
      globalWorkSize = roundUp(localWorkSize, elementCount);         // rounded up to the nearest multiple of the localWorkSize	    
      
      clForces = s.context.createFloatBuffer(globalWorkSize, READ_ONLY);	  
      clMasses = s.context.createFloatBuffer(globalWorkSize, READ_ONLY);
      clStates = s.context.createIntBuffer(globalWorkSize, READ_ONLY);
      
      clPositions = new CLBuffer[2];
      clVelocities = new CLBuffer[2];      
      clPositions[0] = s.context.createFloatBuffer(globalWorkSize, READ_WRITE);
      clPositions[1] = s.context.createFloatBuffer(globalWorkSize, READ_WRITE);
      clVelocities[0] = s.context.createFloatBuffer(globalWorkSize, READ_WRITE);
      clVelocities[1] = s.context.createFloatBuffer(globalWorkSize, READ_WRITE);
      
      currRead = 0;
      currWrite = 1;
      
      // Copy masses, positions and velocities only once: 
      FloatBuffer masses = clMasses.getBuffer();
      FloatBuffer pos = clPositions[currRead].getBuffer();
      FloatBuffer vel = clVelocities[currRead].getBuffer();
      masses.clear();
      pos.clear();
      vel.clear();
      for ( int i = 0; i < s.numberOfParticles(); i++ ) {
        Particle p = s.getParticle( i );
        float mass = p.mass();
        masses.put(mass); masses.put(mass); masses.put(mass);
        pos.put(p.position().x()); pos.put(p.position().y()); pos.put(p.position().z());
        vel.put(p.velocity().x()); vel.put(p.velocity().y()); vel.put(p.velocity().z());
      }
      masses.rewind();
      pos.rewind();
      vel.rewind();
      
      if (!init) {        
        URL url = this.getClass().getResource("ModEuler.cl");
        String filename = url.toString();
        String[] lines = s.p.loadStrings(filename);
        String source = "";
        for (int i = 0; i < lines.length; i++) {
          source += lines[i] + '\n';
        }
        
        /*
        try {
          program = s.context.createProgram(getClass().getResourceAsStream("JoglInterop.cl"));
          program.build();
          System.out.println(program.getBuildStatus());
          System.out.println(program.isExecutable());
          System.out.println(program.getBuildLog());
        } catch (IOException ex) {
        }
        */
        
        PApplet.println(source);
        program = s.context.createProgram(source).build();          
        kernel = program.createCLKernel("TimeStep");
        init = true;
      }     
      
      
      // A, B are input buffers, C is for the result
      clBufferA = new CLBuffer[2];
      clBufferA[0] = s.context.createFloatBuffer(globalWorkSize, READ_WRITE, USE_BUFFER);
      clBufferA[1] = s.context.createFloatBuffer(globalWorkSize, READ_WRITE, USE_BUFFER);
      clBufferB = s.context.createFloatBuffer(globalWorkSize, READ_ONLY);
      
      
      fillBuffer(clBufferA[0].getBuffer(), 1);
      fillBuffer(clBufferB.getBuffer(), 1);
      
      PApplet.println("Done");
	  }

    // Copy forces and states every time:
	  FloatBuffer forces = clForces.getBuffer();
	  IntBuffer states = clStates.getBuffer();
	  forces.clear();
	  states.clear();
    for ( int i = 0; i < s.numberOfParticles(); i++ ) {
      Particle p = s.getParticle( i );
      int isfree = p.isFree() ? 1 : 0;
      forces.put(p.force().x()); forces.put(p.force().y()); forces.put(p.force().z());      
      states.put(1); states.put(1); states.put(1);
    }
	  forces.rewind();
	  states.rewind();
    
    kernel.putArgs(clMasses, clStates, clForces, 
                   clPositions[currRead], clVelocities[currRead],
                   clPositions[currWrite], clVelocities[currWrite])
          .putArg(t).putArg(elementCount).rewind();    


    // asynchronous write of data to GPU device,
    // followed by blocking read to get the computed results back.
    long time = nanoTime();
    
    
    s.queue.putWriteBuffer(clMasses, false)
           .putWriteBuffer(clStates, false)
           .putWriteBuffer(clForces, false)
           .putWriteBuffer(clPositions[currRead], false)
           .putWriteBuffer(clVelocities[currRead], false)
           .put1DRangeKernel(kernel, 0, globalWorkSize, localWorkSize)
           .putReadBuffer(clPositions[currWrite], true)
           .putReadBuffer(clVelocities[currWrite], true);          
           
    
    //s.queue.put1DRangeKernel(kernel, 0, globalWorkSize, localWorkSize);
    
    
    // Copying back to the system so the particles can be drawn in the correct locations.
    FloatBuffer pos = clPositions[currWrite].getBuffer();
    FloatBuffer vel = clVelocities[currWrite].getBuffer();
    pos.rewind();
    vel.rewind();
    for ( int i = 0; i < s.numberOfParticles(); i++ ) {
      Particle p = s.getParticle( i );
      
      p.position().setX(pos.get());
      p.position().setY(pos.get());
      p.position().setZ(pos.get());
      
      p.velocity().setX(vel.get());
      p.velocity().setY(vel.get());
      p.velocity().setZ(vel.get());

      //PApplet.println(t + " " + i + " " + p.position().x + " " + p.position().y + " " + p.position().z);
	  }
    //PApplet.println(pos.remaining() + " " + currRead + " " + currWrite + " --------------------------");
    pos.rewind();
    vel.rewind();
    
    
    /*
    kernel.putArgs(clBufferA[currRead], clBufferB, clBufferA[currWrite]).putArg(elementCount).rewind();

    // asynchronous write of data to GPU device,
    // followed by blocking read to get the computed results back.
    long time = nanoTime();
    s.queue.putWriteBuffer(clBufferA[currRead], false)
         .putWriteBuffer(clBufferB, false)
         .put1DRangeKernel(kernel, 0, globalWorkSize, localWorkSize)
         .putReadBuffer(clBufferA[currWrite], true);
    time = nanoTime() - time;    

    // print first few elements of the resulting buffer to the console.
    out.println("a+b=c results snapshot: ");
    FloatBuffer buf = clBufferA[currWrite].getBuffer();
    buf.rewind();
    for(int i = 0; i < 10; i++)
        out.print(buf.get() + ", ");
    out.println("...; " + buf.remaining() + " more");

    out.println("computation took: "+(time/1000000)+"ms");    
    */
	  
    /*
    CLBuffer<FloatBuffer> temp = clBufferA;
    clBufferA = clBufferC;
    clBufferC = temp;
      */
    
    
    int temp = currRead;
    currRead =  currWrite;
    currWrite = temp;
    
	}

  private static void fillBuffer(FloatBuffer buffer, int seed) {
    Random rnd = new Random(seed);
    while(buffer.remaining() != 0)
        //buffer.put(rnd.nextFloat()*100);
      buffer.put(seed);
    buffer.rewind();
}	
	
	private static int roundUp(int groupSize, int globalSize) {
    int r = globalSize % groupSize;
    if (r == 0) {
        return globalSize;
    } else {
        return globalSize + groupSize - r;
    }
}	
	
}
